package com.burgstaller.aGTD.sync;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeSet;
import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import android.app.AlertDialog;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.content.MergeResult;
import android.content.SharedPreferences;
import android.content.SyncAdapter;
import android.content.SyncContext;
import android.content.TrivialSyncData;
import android.database.Cursor;
import android.net.Uri;
import android.text.util.Regex;
import android.util.Log;

import com.burgstaller.aGTD.AGtdContentProvider;
import com.burgstaller.aGTD.R;
import com.burgstaller.aGTD.R.string;
import com.burgstaller.provider.AGtd;
import com.google.wireless.gdata.data.StringUtils;

import de.trantor.mail.ImapClient;
import de.trantor.mail.MailException;
import de.trantor.mail.Message;

public class AgtdSyncAdapter extends SyncAdapter {
	
	private static final String MSG_HEADERKEY_DUE = "X-GTD-Due";
	private static final String MSG_HEADERKEY_START = "X-GTD-Start";
	private static final String MSG_HEADERKEY_PROJ_NAME = "X-GTD-Project";
	private static final String MSG_HEADERKEY_CONTEXT_NAME = "X-GTD-Context";
	private static final String MSG_HEADERKEY_CONTEXT_ID = "X-aGTD-CID";
	private static final String MSG_HEADERKEY_PROJ_ID = "X-aGTD-PID";
	private static final String MSG_HEADERKEY_ID = "X-aGTD-ID";

	public class IntegerComparator implements Comparator<Integer> {
		public int compare(Integer object1, Integer object2) {
			if (object1 == null && object2 == null) { 
				return 0;
			} else if (object1 == null) {
				return -1;
			} else {
				return object1.compareTo(object2);
			}
		}
	}

	public class SyncRecord {
		public int m_localId;
		public int m_remoteId;
		public String m_title;
		public int m_oldRemoteId = -1;

		public String toString() {
			return "SyncRecord[locUID: " + m_localId + ", remUID: " 
				+ m_remoteId + ", title: " + m_title + "]";
		}

	}
	
	public class RemoteSyncRecord extends SyncRecord {
		public Message m_newMessage;
		/** Extra headers used for synching other clients. */
		public HashMap<String, String> m_extraHeaders;
		
		public void appendHeader(String key, String value) {
			if (m_extraHeaders == null) {
				m_extraHeaders = new HashMap<String, String>();
			}
			m_extraHeaders.put(key, value);
		}
		
		public String toString() {
			StringBuilder builder = new StringBuilder("RemoteSyncRecord[remUID: " + m_remoteId
				+ ", localId:" + m_localId
				+ ", title: " 
				+ m_title + ", msg: " + m_newMessage);
			
			if (m_extraHeaders != null && m_extraHeaders.size() > 0) {
				builder.append(" ExtraHeaders:\n");
				for (String key: m_extraHeaders.keySet()) {
					builder.append("  ");
					builder.append(m_extraHeaders.get(key));
					builder.append("\n");
				}
			}
			builder.append("]");
			return builder.toString();
		}
	}
	
	public class AgtdSyncData extends TrivialSyncData {
		private Vector<SyncRecord> m_added;
		private Vector<SyncRecord> m_modified;
		private Vector<SyncRecord> m_deleted;
		private Vector<SyncRecord> m_staleRecords;

		void setAdded(Vector<SyncRecord> data) {
			m_added = data;
		}
		
		public Vector<SyncRecord> getAdded() {
			return m_added;
		}

		public void setModified(Vector<SyncRecord> data) {
			m_modified = data;
		}

		public Vector<SyncRecord> getModified() {
			return m_modified;
		}
		/**
		 * @return the deleted
		 */
		public Vector<SyncRecord> getDeleted() {
			return m_deleted;
		}

		/**
		 * @param deleted the deleted to set
		 */
		public void setDeleted(Vector<SyncRecord> deleted) {
			m_deleted = deleted;
		}
		
		public String toString() {
			StringBuffer buf = new StringBuffer();
			buf.append("AgtdSyncData\n");
			buf.append(debugRecordVector("Added", m_added));
			buf.append(debugRecordVector("Modified", m_modified));
			buf.append(debugRecordVector("Deleted", m_deleted));
			if (m_staleRecords != null && m_staleRecords.size() > 0) {
				buf.append(debugRecordVector("Stale", m_staleRecords));
			}
			return buf.toString();
		}
		
		public String debugRecordVector(String headline, Vector<SyncRecord> list) {
			StringBuilder buf = new StringBuilder();
			buf.append("\n\n  ");
			buf.append(headline);
			buf.append(list.size());
			for (Object record : list) {
				buf.append("\n");
				buf.append(record.toString());
			}
			buf.append("\n");
			return buf.toString();
		}

		public void setStaleRecords(Vector<SyncRecord> staleRecords) {
			m_staleRecords = staleRecords;
		}

		public Vector<SyncRecord> getStaleRecords() {
			return m_staleRecords;
		}

		public boolean hasStaleRecords() {
			return m_staleRecords != null && m_staleRecords.size() > 0;
		}
	}
	
	private String m_user;
	private String m_pass;
	private String m_imapHost;
	ImapClient m_client = new ImapClient();
	
	private static final String[] SYNC_PROJECTION = new String[] {
		AGtd.Tasks.SYNC_STATUS,	// 0 
		AGtd.Tasks.SYNC_DATE, 	// 1
		AGtd.Tasks._ID,			// 2
		AGtd.Tasks.EXTERNAL_ID,	// 3
		AGtd.Tasks.TITLE		// 4
	};
	
	private static final int SYNC_STATUS_INDEX = 0;
	private static final int SYNC_DATE_INDEX = 1;
	private static final int ID_INDEX = 2;
	private static final int EXTERNAL_ID_INDEX = 3;
	private static final int TITLE_INDEX = 4;
	
	/**
	 * The columns we are interested in from the database 
	 * for full access
	 */
	public static final String[] FULL_PROJECTION = new String[] {
			AGtd.Tasks._ID, 
			AGtd.Tasks.TITLE, 			// 1 
            AGtd.Tasks.MODIFIED_DATE, 	// 2
            AGtd.Tasks.FK_CONTEXT_ID, 	// 3
            AGtd.Tasks.FK_PROJECT_ID, 	// 4
            AGtd.Tasks.COMPLETED,		// 5
            AGtd.Tasks.NOTES,			// 6
            AGtd.Tasks.START_DATE,		// 7
            AGtd.Tasks.DUE_DATE,		// 8
            AGtd.Tasks.SYNC_STATUS,		// 9
            AGtd.Tasks.SYNC_DATE,		// 10
    		AGtd.Tasks.EXTERNAL_ID,		// 11
    		AGtd.Tasks.EXTRA_SYNC_INFO	// 12
	};
	
	public static final int FULL_ID_INDEX = 0;
	public static final int FULL_TITLE_INDEX = 1;
	public static final int FULL_MODIFIFIED_INDEX = 2;
	public static final int FULL_CONTEXT_ID_INDEX = 3;
	public static final int FULL_PROJECT_ID_INDEX = 4;
	public static final int FULL_COMPLETED_INDEX = 5;
	public static final int FULL_NOTES_INDEX = 6;
	public static final int FULL_STARTDATE_INDEX = 7;
	public static final int FULL_DUEDATE_INDEX = 8;
	public static final int FULL_SYNC_STATUS_INDEX = 9;
	public static final int FULL_SYNCDATE_INDEX = 10;
	public static final int FULL_EXTERNAL_ID_INDEX = 11;
	public static final int FULL_EXTRA_SYNC_INFO_INDEX = 12;

	private long m_lastSyncTimestamp;
	private HashMap<Integer, Integer> m_uidTable;
	
	public AgtdSyncAdapter(Context context) {
		SharedPreferences prefs = context.getSharedPreferences(
				AGtd.SETTINGS_NAME, Context.MODE_PRIVATE);
		m_lastSyncTimestamp = prefs.getLong(AGtd.Settings.LAST_SYNC_DATE, 
				0);
		m_imapHost = prefs.getString(AGtd.Settings.IMAP_SERVER, "");
		m_user = prefs.getString(AGtd.Settings.IMAP_USER, "");
		m_pass= prefs.getString(AGtd.Settings.IMAP_PASS, "");
		setContext(context);
		if (m_imapHost.length() <= 0) {
			throw new RuntimeException(context.getString(R.string.error_no_imap_settings));
		}
	}
	
	@Override
	public SyncData getServerDiffs(SyncContext synccontext,
			ContentProvider contentprovider, Uri contenturi, Map map) {
		return getServerChanges();
	}

	/**
	 * this method will check the server and search for changes to be
	 * applied to the local data.
	 * @return a syncdata object containing {@link RemoteSyncRecord}.
	 */
	private SyncData getServerChanges() {
		AgtdSyncData result = new AgtdSyncData();
		Vector changed = new Vector();
		try {
			openClient();
			
			// first check if we need to add tasks to the local db
			HashMap<Integer, Integer> uidTable = getUidIndexTable(m_client);
			m_uidTable = uidTable;
			determineServerAdditionsAndModifications(result, changed,
					uidTable);
			
			determineServerDeletions(result, uidTable);
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			Log.e(AGtd.LOG_TAG, "Error get server changes", e);
		}
		return result;
	}


	private void determineServerDeletions(AgtdSyncData result, 
			HashMap<Integer, Integer> uidTable) {
		Vector deleted = new Vector();

		// then check if there have been tasks that should be removed from
		// the local DB
		Cursor deletedCursor = getContext().getContentResolver().query(
				AGtd.Tasks.CONTENT_URI, 
				SYNC_PROJECTION, AGtd.Tasks.SYNC_DATE + " < ? AND " 
				+ AGtd.Tasks.SYNC_STATUS + " = ? AND " + AGtd.Tasks.COMPLETED 
				+ " = 0", new String [] {
				"" + System.currentTimeMillis(), 
				"" + AGtd.Tasks.SYNC_STATUS_SYNCED}, AGtd.Tasks._ID 
				+ " ASC");
		int id = -1;
		while (deletedCursor.next()) {
			id = deletedCursor.getInt(EXTERNAL_ID_INDEX);
			if (uidTable.get(id) == null) {
				String title = deletedCursor.getString(TITLE_INDEX);
				Log.d(AGtd.LOG_TAG, "deleted on the server: " + id + ": " 
						+ title);
				RemoteSyncRecord record = new RemoteSyncRecord();
				record.m_remoteId = id;
				record.m_localId = deletedCursor.getInt(ID_INDEX);
				record.m_title = title;
				deleted.add(record);
			}
		}
		Log.i(AGtd.LOG_TAG, "to be deleted locally: " + deleted.size());		
		result.setDeleted(deleted);
		AGtdContentProvider.popCursor(deletedCursor);
		deletedCursor.close();
		deletedCursor = null;
	}

	private void determineServerAdditionsAndModifications(AgtdSyncData result,
			Vector changed, HashMap<Integer, Integer> uidTable) 
				throws IOException,	MailException {
		Vector<SyncRecord> staleRecords = new Vector();
		Vector added = new Vector<SyncRecord>();
		
		Log.d(AGtd.LOG_TAG, "have " + uidTable.keySet().size() + " mails");
		for (Integer uid : uidTable.keySet()) {
			Log.d(AGtd.LOG_TAG, "  processing id: " + uid);
			Cursor taskCursor = findTaskByUid(uid);
			if (taskCursor.first()) {
				// in IMAP records cannot really be changed while
				// maintaining the same uid. Therefore, we assume
				// that everything is the same.
			} else {
				RemoteSyncRecord record = new RemoteSyncRecord();
				Message msg = m_client.getMessage(uidTable.get(uid));
				record.m_newMessage = msg;
				record.m_remoteId = uid;
				record.m_title = record.m_newMessage.getHeaderValue("Subject");
				
				String aId = msg.getHeaderValue(MSG_HEADERKEY_ID);
				if (aId != null) {
					// this record has been synched before, if we dont find
					// it anymore, then it was already deleted locally. 
					// in such a case this is a stale record and we should
					// ask the user if he is ok to delete that.
					int localId = Integer.parseInt(aId);
					Cursor localRecord = findFullTaskById(localId);
					if (localRecord.first()) {
						record.m_localId = localId;
						changed.add(record);
						Log.i(AGtd.LOG_TAG, " changed: " 
								+ record.toString());
					} else {
						Log.i(AGtd.LOG_TAG, " stale: " 
								+ record.toString());
						
						staleRecords.add(record);
					}
					AGtdContentProvider.popCursor(localRecord);
					localRecord.close();
					localRecord = null;
				} else {
					added.add(record);
					Log.i(AGtd.LOG_TAG, " added: " + record.toString());
				}
			}
			taskCursor.close();
		}
		result.setAdded(added);
		result.setModified(changed);
		result.setStaleRecords(staleRecords);
		Log.i(AGtd.LOG_TAG, "to be added locally: " + added.size());
		Log.i(AGtd.LOG_TAG, "to be changed locally: " + changed.size());
	}

	public void closeClient() {
		try {
			m_client.close();
			m_client = null;
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	public void openClient() throws Exception, IOException, MailException {
		if (m_client == null) {
			m_client = new ImapClient();
		}
		m_client.open(m_imapHost, m_user, m_pass);
	}

	private Cursor findTaskByUid(Integer uid) {
		return getContext().getContentResolver().query(AGtd.Tasks.CONTENT_URI, 
				SYNC_PROJECTION, AGtd.Tasks.EXTERNAL_ID + " = ?" 
				, new String[] {uid.toString()}, null); 
	}
	
	public Cursor findFullTaskById(int id) {
		return getContext().getContentResolver().query(
				AGtd.Tasks.CONTENT_URI, FULL_PROJECTION, AGtd.Tasks._ID + "=?", new String[]{"" + id}, null);
	}
	
	/**
	 * lookup method that searches for a context name.
	 * @param contextId the id of the context (local Id)
	 * @return the name of the context, INBOX if not found.
	 */
	private String findContextNameForId(int contextId) {
		Cursor contextCursor = getContext().getContentResolver().query(
				AGtd.Contexts.CONTENT_URI, new String[] {AGtd.Contexts.TITLE}, 
				AGtd.Contexts._ID + "=?", new String[] {"" + contextId}, null);
		String result = "INBOX";
		if (contextCursor.first()) {
			result = contextCursor.getString(0);
		}
		AGtdContentProvider.popCursor(contextCursor);
		return result;
	}


	/**
	 * lookup method that searches for a context name.
	 * @param contextName the name of the context
	 * @return the id of the context, the INBOX id if not found.
	 */
	private int findContextIdForName(String contextName) {
		Cursor contextCursor = getContext().getContentResolver().query(
				AGtd.Contexts.CONTENT_URI, new String[] {AGtd.Contexts._ID}, 
				AGtd.Contexts.TITLE + "=?", new String[] {contextName}, null);
		int result = AGtd.Contexts.INBOX_RECORD_ID;
		if (contextCursor.first()) {
			result = contextCursor.getInt(0);
		}
		AGtdContentProvider.popCursor(contextCursor);
		contextCursor.close();
		return result;
	}


	/**
	 * lookup method that searches for a project name.
	 * @param projectName the name of the project
	 * @return the id of the project, -1
	 */
	private int findProjectIdForName(String projectName) {
		Cursor projectCursor = getContext().getContentResolver().query(
				AGtd.Projects.CONTENT_URI, new String[] {AGtd.Projects._ID}, 
				AGtd.Projects.TITLE + "=?", new String[] {projectName}, null);
		int result = -1;
		if (projectCursor.first()) {
			result = projectCursor.getInt(0);
		}		
		AGtdContentProvider.popCursor(projectCursor);
		return result;
	}

	/**
	 * lookup mehtod that searches for the corresponding project name
	 * when being given the project id.
	 * @param projectId the id of the project for which we want to know
	 * 		the name.
	 * @return the name of the project, "NONE" if not found.
	 */
	private String findProjectNameForId(int projectId) {
		Cursor projectCursor = getContext().getContentResolver().query(
				AGtd.Projects.CONTENT_URI, new String[] {AGtd.Projects.TITLE}, 
				AGtd.Projects._ID + "=?", new String[] {"" + projectId}, null);
		String result = "NONE";
		if (projectCursor.first()) {
			result = projectCursor.getString(0);
		}
		AGtdContentProvider.popCursor(projectCursor);
		projectCursor.close();
		return result;
	}

	HashMap<Integer, Integer> getUidIndexTable(ImapClient client)
			throws IOException, MailException {
		int count = client.getMessageCount();
		String[] message;
		HashMap<Integer, Integer> result = new HashMap<Integer, Integer>();
		for (int i = 0; i < count; i++) {
			message = client.getUidAndFlags(i);
			if (!message[1].toLowerCase().contains("\\deleted")) {
				Integer uid = new Integer(message[0]);
				result.put(uid, new Integer(i));
			}
		}
		return result;
	}

	@Override
	public boolean isReadOnly() {
		return false;
	}

	@Override
	public SyncAdapter.SyncData readSyncData(ContentProvider contentprovider) {
		return getLocalChanges(m_lastSyncTimestamp);
	}

	/**
	 * this method checks the local data for changes which need
	 * to be applied to the server.
	 * @param sinceTimestamp	last synchronization timestamp
	 * @return	a syncdata object that contains {@link SyncRecord}.
	 */
	private SyncAdapter.SyncData getLocalChanges(long sinceTimestamp) {
		AgtdSyncData result = new AgtdSyncData();
		
		// check for new records
		Cursor cursor = getContext().getContentResolver().query(
				AGtd.Tasks.CONTENT_URI, 
				SYNC_PROJECTION, AGtd.Tasks.SYNC_STATUS + " = " 
				+ AGtd.Tasks.SYNC_STATUS_NEW, 
				null, AGtd.Tasks._ID + " ASC");
		Log.d(AGtd.LOG_TAG, "added " + cursor.count() + " record");
		Vector<SyncRecord> data = new Vector<SyncRecord>();
		while (cursor.next()) {
			dumpCursor(cursor);
			SyncRecord record = new SyncRecord();
			record.m_localId = cursor.getInt(ID_INDEX);
			record.m_remoteId = -1;
			record.m_title = cursor.getString(TITLE_INDEX);
			data.add(record);
			Log.d(AGtd.LOG_TAG, "added " + record);
		}
		result.setAdded(data);
		AGtdContentProvider.popCursor(cursor);
		cursor = null;
		
		// fetch the records that were modified lately
		cursor = getContext().getContentResolver().query(
				AGtd.Tasks.CONTENT_URI, 
				SYNC_PROJECTION, AGtd.Tasks.SYNC_STATUS + " = " 
				+ AGtd.Tasks.SYNC_STATUS_MODIFIED, 
				null, AGtd.Tasks._ID + " ASC");
		Log.d(AGtd.LOG_TAG, "modified " + cursor.count() + " record");
		data = new Vector<SyncRecord>();
		while (cursor.next()) {
			dumpCursor(cursor);
			SyncRecord record = new SyncRecord();
			record.m_localId = cursor.getInt(ID_INDEX);
			record.m_remoteId = cursor.getInt(EXTERNAL_ID_INDEX);
			record.m_title = cursor.getString(TITLE_INDEX);
			data.add(record);
			Log.d(AGtd.LOG_TAG, "modified " + record);
		}
		result.setModified(data);
		AGtdContentProvider.popCursor(cursor);
		cursor = null;

		
		
		// check for deleted records
		cursor = getContext().getContentResolver().query(
				AGtd.Tasks.CONTENT_URI, 
				SYNC_PROJECTION, AGtd.Tasks.SYNC_STATUS + " = " 
				+ AGtd.Tasks.SYNC_STATUS_DELETED, 
				null, AGtd.Tasks._ID + " ASC");
		Log.d(AGtd.LOG_TAG, "deleted " + cursor.count() + " record");
		data = new Vector<SyncRecord>();
		while (cursor.next()) {
			dumpCursor(cursor);
			SyncRecord record = new SyncRecord();
			record.m_localId = cursor.getInt(ID_INDEX);
			record.m_remoteId = cursor.getInt(EXTERNAL_ID_INDEX);
			record.m_title = cursor.getString(TITLE_INDEX);
			data.add(record);
		}
		
		
		result.setDeleted(data);

		AGtdContentProvider.popCursor(cursor);
		cursor = null;
		return result;
	}

	private void dumpCursor(Cursor cursor) {
		String [] headers = cursor.getColumnNames();
		Log.d(AGtd.LOG_TAG, "RECORD");
		for (int i = 0; i < headers.length; i++) {
			Log.d(AGtd.LOG_TAG, " Header: " + headers[i] + ": " 
					+ cursor.getString(i));
		}
	}

	/**
	 * this method uploads the local changes to the server.
	 * @param synccontext the context (currently unused)
	 * @param contentprovider the content provider in which the
	 * 		changes should be put. Since the sync framework
	 * 		is not properly working at the moment, we just
	 * 		ignore this for now.
	 */
	@Override
	public MergeResult sendClientDiffs(SyncContext synccontext,
			ContentProvider contentprovider) {
		// TODO Auto-generated method stub
		return new MergeResult();
	}

	@Override
	public void syncCancelled() {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void syncEnding(SyncContext synccontext, boolean flag) {
		// TODO Auto-generated method stub
		
	}

	@Override
	public boolean syncStarting(SyncContext synccontext, String account) {
		// TODO Auto-generated method stub
		return true;
	}

	/**
	 * this method will now apply the local changes to the server.
	 * @param localChanges the syncdata object with local modifications
	 */
	public void sendClientDiffs(AgtdSyncData localChanges) {
		
		// first process the modifications
		Vector<SyncRecord> toModify = localChanges.getModified();
		Vector<SyncRecord> toDelete = new Vector<SyncRecord>(
				localChanges.getDeleted());
		try {
			Log.i(AGtd.LOG_TAG, "uploading for modifications");
			for (SyncRecord record: toModify) {
				Cursor fullCursor = findFullTaskById(record.m_localId);
				if (!fullCursor.first()) {
					AGtdContentProvider.popCursor(fullCursor);
					fullCursor = null;
					throw new IllegalStateException(
							"Did not find local record with id: " 
							+ record.m_localId + ": " + record);
				}
				
				// check if the record has to be deleted
				if (fullCursor.getInt(FULL_COMPLETED_INDEX) != 0) {
					Log.d(AGtd.LOG_TAG, "removing remote message, since" 
							+ " local record completed: " + record);
					toDelete.add(record);
				} else {
					// a modification is a delete + add new  
					Log.i(AGtd.LOG_TAG, " process modified: " 
							+ record.toString());
					record.m_oldRemoteId = record.m_remoteId;
					toDelete.add(record);
					uploadLocalRecord(record, fullCursor);
				}
				
				markAsSynced(fullCursor);
				AGtdContentProvider.popCursor(fullCursor);
				fullCursor = null;
			}
			
			
			uploadAdditions(localChanges.getAdded());
			deleteRemoteMessages(toDelete);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	private void deleteRemoteMessages(Vector<SyncRecord> messageList) 
			throws IOException, MailException {
		if (messageList == null || messageList.size() <= 0) {
			return;
		}
		// now check the deletions in sorted order
		Log.i(AGtd.LOG_TAG, "Deleting remote messages: " 
				+ messageList.size());
		TreeSet<Integer> indexList = new TreeSet<Integer>(new IntegerComparator());
		for (SyncRecord record : messageList) {
			Integer index = m_uidTable.get(new Integer(record.m_remoteId));
			if (index == null && record.m_oldRemoteId > 0) {
				index = m_uidTable.get(new Integer(record.m_oldRemoteId));
			}
			if (index != null) {
				indexList.add(index);
			}
			markAsSynced(record);
		}
		deleteRemoteMessagesByIndex(indexList);
		
		// as stated before, the uidtable needs to be re-read
		m_uidTable = getUidIndexTable(m_client);
	}

	private void deleteRemoteMessagesByIndex(TreeSet<Integer> indexList) {
		for (Integer index : indexList) {
			deleteRemoteMessage(index);
		}
	}


	public void deleteStaleMessages(Vector<RemoteSyncRecord> records) 
				throws IOException, MailException {
		// we have to delete records on the server, therefore
		// all the indices will change and we need to rerequest
		// the uid hashtable afterwards.
		TreeSet<Integer> indexList = new TreeSet<Integer>(new IntegerComparator());
		for (RemoteSyncRecord re : records) {
			int index = m_uidTable.get(new Integer(re.m_remoteId));
			Log.i(AGtd.LOG_TAG, "deleting stale remote message index: " 
					+ index + ": " + re.toString());
			indexList.add(index);
		}
		// now the list is sorted
		deleteRemoteMessagesByIndex(indexList);
		
		// as stated before, the uidtable needs to be re-read
		m_uidTable = getUidIndexTable(m_client);
	}
	
	private void uploadAdditions(Vector <SyncRecord> toAdd)
			throws MailException {
		Log.i(AGtd.LOG_TAG, "upload additions");

		for (SyncRecord record: toAdd) {
			Cursor fullCursor = findFullTaskById(record.m_localId);
			if (!fullCursor.first()) {
				throw new IllegalStateException(
						"Did not find local record with id: " 
						+ record.m_localId + ": " + record);
			}
			uploadLocalRecord(record, fullCursor);
			fullCursor.updateInt(FULL_EXTERNAL_ID_INDEX, record.m_remoteId);
			markAsSynced(fullCursor);
		}
	}

	private void markAsSynced(SyncRecord record) {
		ContentValues values = new ContentValues();
		values.put(AGtd.Tasks.SYNC_STATUS, AGtd.Tasks.SYNC_STATUS_SYNCED);
		values.put(AGtd.Tasks.SYNC_DATE, System.currentTimeMillis());
		getContext().getContentResolver().update(AGtd.Tasks.CONTENT_URI, 
				values, AGtd.Tasks._ID + "=?", 
				new String[] {"" + record.m_localId});
	}

	private void markAsSynced(Cursor fullCursor) {
		fullCursor.updateInt(FULL_SYNC_STATUS_INDEX, AGtd.Tasks.SYNC_STATUS_SYNCED);
		fullCursor.updateLong(FULL_SYNCDATE_INDEX, System.currentTimeMillis());
		fullCursor.commitUpdates();		
	}

	/**
	 * this method creates a new message from a local DB record / task.
	 * @param record	the newly created record
	 * @param fullCursor the cursor to the full record data.
	 * @throws MailException if there is something wrong with the mail communication
	 */
	private void uploadLocalRecord(SyncRecord record, Cursor fullCursor) throws MailException {
		// create a message for the local record
		Message mailMessage = new Message("aGTD", m_user, 
				fullCursor.getString(FULL_TITLE_INDEX));
		BufferedReader reader = new BufferedReader(new StringReader(
				fullCursor.getString(FULL_NOTES_INDEX)));

		int contextId = fullCursor.getInt(FULL_CONTEXT_ID_INDEX);
		int projectId = fullCursor.getInt(FULL_PROJECT_ID_INDEX);
		// store most of the fields in headers
		mailMessage.addHeaderLine(MSG_HEADERKEY_ID + ": " + record.m_localId);
		mailMessage.addHeaderLine(MSG_HEADERKEY_PROJ_ID + ": " + projectId);
		mailMessage.addHeaderLine(MSG_HEADERKEY_CONTEXT_ID + ": " + contextId);
		
		mailMessage.addHeaderLine(MSG_HEADERKEY_CONTEXT_NAME + ": " + findContextNameForId(contextId));
		mailMessage.addHeaderLine(MSG_HEADERKEY_PROJ_NAME + ": " + findProjectNameForId(projectId));
		addExtraSyncHeaders(fullCursor, mailMessage);
		long startMs = fullCursor.getLong(FULL_STARTDATE_INDEX);
		long dueMs = fullCursor.getLong(FULL_DUEDATE_INDEX);
		if (startMs > 0) {
			mailMessage.addHeaderLine(MSG_HEADERKEY_START + ": " 
					+ SimpleDateFormat.getDateInstance().format(
							new Date(startMs)));
		}
		if (dueMs > 0) {
			mailMessage.addHeaderLine(MSG_HEADERKEY_DUE + ": " 
					+ SimpleDateFormat.getDateInstance().format(
							new Date(dueMs)));
		}
		mailMessage.addBodyLine("Notes:");
		String line = "";
		try {
			while ((line = reader.readLine()) != null) {
				mailMessage.addBodyLine(line);
			}
		} catch (IOException e1) {
			e1.printStackTrace();
		}
		Matcher match = null;
		try {
			String appendResult = m_client.appendMessage(mailMessage, "\\Seen");
			Pattern rePattern = Pattern.compile("\\[APPENDUID\\s+(\\w+)\\s+(\\w+)\\]");
			match = rePattern.matcher(appendResult);
		} catch (Exception e) {
			Log.e(AGtd.LOG_TAG, "error uploading new message: " + record 
					+ ", " + e);
			e.printStackTrace();
		}
		if (!match.find()) {
			Log.e(AGtd.LOG_TAG, "IMAP server does not support UIDPLUS");
			throw new MailException(
					"IMAP Server does not support required UIDPLUS " +
					"extension (http://www.ietf.org/rfc/rfc2359.txt)"); 
		}
		// we have found the uid
		String uid = match.group(2);
		int remoteUid = Integer.parseInt(uid);
		record.m_remoteId = remoteUid;
		Log.i(AGtd.LOG_TAG, "Upload of record: " + record.toString() 
				+ " successful");
		// finally update the new uid of the mail in the local DB
		fullCursor.updateInt(FULL_EXTERNAL_ID_INDEX, remoteUid);
	}

	private void addExtraSyncHeaders(Cursor fullCursor, Message mailMessage) {
		String extraHeaders = fullCursor.getString(FULL_EXTRA_SYNC_INFO_INDEX);
		if (extraHeaders != null && extraHeaders.length() > 0) {
			BufferedReader reader = new BufferedReader(
					new StringReader(extraHeaders));
			String line = null;
			try {
				while ((line = reader.readLine()) != null) {
					mailMessage.addHeaderLine(line);
				}
			} catch (IOException e) {
				Log.e(AGtd.LOG_TAG, "Error adding extra header: " + e);
				e.printStackTrace();
			}
		}
	}

	/**
	 * deletes a remote message on the server
	 * @param record	the sync record that contains the 
	 * 		remote uid.
	 */
	private void deleteRemoteMessage(SyncRecord record) {
		Integer index = m_uidTable.get(new Integer(record.m_remoteId));
		if (index == null) {
			Log.e(AGtd.LOG_TAG,"did not find index for uid: " 
					+ record.m_remoteId);
			return;
		}
		deleteRemoteMessage(index);
	}

	private void deleteRemoteMessage(Integer index) {
		Log.i(AGtd.LOG_TAG, "sendClientDiffs: deleting on server record: " 
				+ index);
		try {
			m_client.removeMessage(index);
		} catch (Exception e) {
			Log.w(AGtd.LOG_TAG, "Unable to delete message index: " + index, e);
			e.printStackTrace();
		}
	}

	private void updateRemoteMessage(SyncRecord record, Cursor fullCursor)
				throws MailException {
		deleteRemoteMessage(record);
		uploadLocalRecord(record, fullCursor);
		fullCursor.updateInt(FULL_EXTERNAL_ID_INDEX, record.m_remoteId);
	}

	/**
	 * this method creates a contentvalues tuple for being applied 
	 * to the local database for {@link RemoteSyncRecord}s.
	 * @param record	the {@link RemoteSyncRecord} containing the changes.
	 * @return the {@link ContentValues} object which can be 
	 * 		used for updating the local database.
	 */
	public ContentValues getContentValuesFor(RemoteSyncRecord record) {
		ContentValues result = new ContentValues();
		result.put(AGtd.Tasks.TITLE, record.m_title);
		result.put(AGtd.Tasks.EXTERNAL_ID, record.m_remoteId);
		result.put(AGtd.Tasks.SYNC_DATE, System.currentTimeMillis());
		result.put(AGtd.Tasks.SYNC_STATUS, AGtd.Tasks.SYNC_STATUS_SYNCED);
		
		StringBuilder extraHeaders = new StringBuilder();
		String contextName = null;
		String projectName = null;
		if (record.m_newMessage != null) {
			String[] names = parseMessage(record.m_newMessage, result, 
					extraHeaders);
			contextName = names[0];
			projectName = names[1];
		}
		if (contextName != null 
				&& !result.containsKey(AGtd.Tasks.FK_CONTEXT_ID)) {
			int contextId = findContextIdForName(contextName);
			if (contextId < 0) {
				contextId = createContext(contextName);
			}
			result.put(AGtd.Tasks.FK_CONTEXT_ID, "" + contextId);
		}
		if (projectName != null 
				&& !result.containsKey(AGtd.Tasks.FK_PROJECT_ID)) {
			int projectId = findProjectIdForName(projectName);
			if (projectId < 0) {
				projectId = createProject(projectName);
			}
			result.put(AGtd.Tasks.FK_CONTEXT_ID, "" + projectId);
		}
		if (extraHeaders.length() > 0) {
			result.put(AGtd.Tasks.EXTRA_SYNC_INFO, extraHeaders.toString());
		}
		return result;
	}

	String [] parseMessage(Message msg, ContentValues result, 
			StringBuilder extraHeaders) {
		String contextName = null;
		String projectName = null;
		
		for (int i = 0; i < msg.getHeaderLineCount(); i++) {
			String headerKey = msg.getHeaderName(i);
			String headerVal = msg.getHeaderValue(i);
			Log.i(AGtd.LOG_TAG, "    header: " + headerKey + ": " + headerVal);
			if (headerKey.startsWith("X-") && headerKey.contains("GTD")) {
				Log.i(AGtd.LOG_TAG, "    gtd header: " + headerKey + ": " + headerVal);
				// process header
				if (MSG_HEADERKEY_CONTEXT_ID.equals(headerKey)) {
					result.put(AGtd.Tasks.FK_CONTEXT_ID, headerVal);
				} else if (MSG_HEADERKEY_CONTEXT_NAME.equals(headerKey)) {
					contextName = headerVal;
				} else if (MSG_HEADERKEY_DUE.equals(headerKey)) {
					long timeVal;
					try {
						timeVal = SimpleDateFormat.getDateInstance().parse(
								headerVal).getTime();
						result.put(AGtd.Tasks.DUE_DATE, timeVal);
					} catch (ParseException e) {
						Log.e(AGtd.LOG_TAG, "Error parsing due", e);
					}
				} else if (MSG_HEADERKEY_ID.equals(headerKey)) {
					// ignore the id for now
				} else if (MSG_HEADERKEY_PROJ_ID.equals(headerKey)) {
					result.put(AGtd.Tasks.FK_PROJECT_ID, headerVal);
				} else if (MSG_HEADERKEY_PROJ_NAME.equals(headerKey)) {
					projectName = headerVal;
				} else if (MSG_HEADERKEY_START.equals(headerKey)) {
					long timeVal;
					try {
						timeVal = SimpleDateFormat.getDateInstance().parse(
								headerVal).getTime();
						result.put(AGtd.Tasks.START_DATE, timeVal);
					} catch (ParseException e) {
						Log.e(AGtd.LOG_TAG, "Error parsing start", e);
					}
				} else {
					// ok these are keys that we dont recognize but we need
					// to store them anyway
					extraHeaders.append(headerKey);
					extraHeaders.append(": ");
					extraHeaders.append(headerVal);
					extraHeaders.append("\n");
				}
			}
		}
		return new String[] {contextName, projectName};
	}
	private int createContext(String contextName) {
		int contextId;
		// create new context
		ContentValues values = new ContentValues();
		values.put(AGtd.Contexts.TITLE, contextName);
		Uri newContext = getContext().getContentResolver().insert(
				AGtd.Contexts.CONTENT_URI, values);
		contextId = Integer.parseInt(newContext.getLastPathSegment());
		return contextId;
	}


	private int createProject(String projectName) {
		int projectId;
		// create new project
		ContentValues values = new ContentValues();
		values.put(AGtd.Projects.TITLE, projectName);
		Uri newProject = getContext().getContentResolver().insert(
				AGtd.Projects.CONTENT_URI, values);
		projectId = Integer.parseInt(newProject.getLastPathSegment());
		return projectId;
	}

}
